import os
import re
import json
import yaml
import shutil
import configparser

from pathlib import Path
from xml.etree import ElementTree
from abc import ABC, abstractmethod
from typing import Optional, Union, Iterable
from django.db.models.query_utils import logger



class AbcFileProcessor(ABC):
    def __init__(self, path):
        self._path = path

    @abstractmethod
    def load(self):
        ...

    @abstractmethod
    def save(self, *args, **kwargs):
        ...

    @property
    def path(self):
        return self._path

    @path.setter
    def path(self, value: Union[str, Path]):
        if isinstance(value, str):
            value = Path(value)
        if value.exists():
            raise FileExistsError('目录或文件不存在')
        self._path = value


class YamlProcessor(AbcFileProcessor):
    """yaml 文件处理器"""

    def load(self, encoding='utf8') -> dict:
        with open(self._path, encoding=encoding) as f:
            data = yaml.load(f, yaml.FullLoader)
        return data

    def read_all(self, encoding='utf-8'):
        with open(self._path, encoding=encoding) as f:
            data = yaml.load_all(f, yaml.FullLoader)
            return data

    def save(self, data, encoding='utf8', mode='a+'):
        with open(self._path, encoding=encoding, mode=mode) as f:
            yaml.dump(data, stream=f, allow_unicode=True)

    def clear(self, encoding='utf8', mode='w'):
        with open(self._path, encoding=encoding, mode=mode) as f:
            f.truncate()


class IniProcessor(AbcFileProcessor):
    """ini 文件处理器"""

    def __init__(self, path):
        super().__init__(path)
        self.config = configparser.ConfigParser()

    def load(self, encoding='utf8'):
        self.config.read(self.path, encoding)

    def save(self, encoding='utf8', *args, **kwargs):
        with open(self._path, 'w', encoding=encoding) as f:
            self.config.write(f)

    def get_node(self):
        """获取所有节点，返回列表"""
        node = self.config.sections()
        return node

    def get_key(self, node):
        """获取指定节点下的键值对"""
        node = self.config.items(node)
        return node

    def get_value(self, node, key):
        """获取指定节点下的指定键的值"""
        try:
            values = self.config.get(node, key)
        except configparser.NoOptionError:
            return f'{node} is not found'
        return values

    def add_node(self, node, key=None, values=''):
        """添加键值对"""
        try:
            self.config.add_section(node)
        except configparser.DuplicateSectionError:
            return f'{node} already exists '
        if key is not None:
            self.config.set(node, key, values)

    def move_node(self, node, key=None):
        """删除节点"""
        if key is not None:
            self.config.remove_option(node, key)
        else:
            try:
                self.config.remove_section(node)
            except configparser.NoSectionError:
                return f'{node} is not found'

    def is_node(self, node):
        """判断节点是否存在"""
        return self.config.has_section(node)

    def is_key(self, node, key):
        """判断节点下的键是否存在"""
        return self.config.has_option(node, key)


class JsonProcessor(AbcFileProcessor):

    def load(self, mode='r', encoding='utf-8') -> dict:
        with open(self._path, mode, encoding=encoding) as f_stream:
            json_data = json.load(f_stream)
        return json_data

    def save(self, data, mode='w', encoding='utf-8') -> bool:
        """保存数据"""
        try:
            with open(self._path, mode, encoding=encoding) as f_stream:
                json.dump(data, f_stream, ensure_ascii=False, indent=2)
        except Exception as e:
            logger.warn('保存文件异常>>>{}', e)
        return True


class NfoProcessor(AbcFileProcessor):
    """处理 .nfo 文件的工具类"""

    def __init__(self, path):
        super().__init__(path)
        self.tree: Optional[ElementTree.ElementTree] = None
        self.root: Optional[ElementTree.Element] = None

    def load(self):
        """加载 .nfo 文件"""
        try:
            self.tree = ElementTree.parse(self._path)
            self.root = self.tree.getroot()
        except ElementTree.ParseError:
            # 尝试使用 lxml 解析
            try:
                from lxml import etree
                parser = etree.XMLParser(recover=True)
                self.tree = etree.parse(self._path, parser)
                self.root = self.tree.getroot()
            except Exception as e:
                raise ValueError(f"无法解析文件: {e}")
        return self

    def save(self, file_path=None):
        """保存 .nfo 文件"""
        save_path = file_path or self._path
        if not save_path:
            raise ValueError("未指定保存路径")

        if self.tree is not None:
            self.tree.write(save_path, encoding='utf-8', xml_declaration=True)
        else:
            raise ValueError("没有可保存的数据")

    def get_value(self, path) -> str:
        """获取指定路径的值"""
        elem = self.root.find(path)
        return elem.text if elem is not None else None

    def get_array_value(self, path) -> set:
        """获取多个值"""
        elem_array = self.root.findall(path)
        return {elem.text for elem in elem_array}

    def set_value(self, path, value):
        """设置指定路径的值"""
        parts = path.split('/')
        current = self.root
        # 创建或获取元素
        for part in parts:
            elem = current.find(part)
            if elem is None:
                elem = ElementTree.SubElement(current, part)
            current = elem

        # 设置值
        current.text = str(value)


class DirectoryProcessor:
    """文件剪切，文件搜索等"""

    @staticmethod
    def search_dir_all_file(path) -> Iterable:
        """获取指定目录下所有文件，包含子文件夹中的文件， 返回文件名，文件所在目录"""
        for file_path, dir_list, file_list in os.walk(path):
            for file in file_list:
                yield os.path.join(file_path, file)

    @staticmethod
    def search_current_dir_file(path) -> Iterable:
        """获取当前目录所有文件"""
        for file in os.listdir(path):
            file_path = os.path.join(path, file)
            if os.path.isfile(file_path):
                yield file_path

    @staticmethod
    def create_dir(path, dirname):
        """创建文件夹"""
        create_dir_path = os.path.join(path, dirname)
        try:
            os.mkdir(create_dir_path)
        except FileExistsError:
            print(f'当文件已存在时，无法创建该文件, {create_dir_path}')
        finally:
            return create_dir_path

    @staticmethod
    def move_file(new_path, old_path, old_file_name=None, new_file_name=None):
        """剪切或重命名文件，当路径相同且文件名不同时，重命名文件
        传参情况分析：
            1.new_path+new_file_name 是完整路径，则 剪切 + 重命名
            2.old_path + old_file_name 必须是完整路径
            3.new_path 和 old_path 都是完整路径，则old_file_name=None, new_file_name=None
        """
        # 判断new_path、old_path 是不是完整的文件路径
        new_file_path = os.path.join(new_path, new_file_name) if os.path.isdir(new_path) and new_file_name else new_path
        old_file_path = old_path if os.path.isfile(old_path) else os.path.join(old_path, old_file_name)
        # new_file_path 有可能是完整的文件路径也有可能是目录路径
        # if os.path.isfile(new_path) or os.path.exists()
        try:
            new_file_path = shutil.move(old_file_path, new_file_path)
            # 返回剪切后文件路径
            return new_file_path
        except FileNotFoundError:
            print(f'No such file or directory: {old_file_path}')

    def rename_file(self, path, old_name, new_name):
        """重命名文件"""

    @staticmethod
    def get_file_name_dir_path(path) -> tuple:
        """获取文件名称|文件夹名称 和上级目录名称"""
        dir_path, file_name = os.path.split(path)
        dir_name = os.path.split(dir_path)[1]
        return dir_path, dir_name, file_name

    @staticmethod
    def get_file_type(file_name: str):
        """获取视频文件后缀"""
        return file_name.rsplit('.', maxsplit=1)[1]

    @classmethod
    def is_video_type(cls, file, video: list = None) -> bool:
        video = video if isinstance(video, Iterable) else []
        if file.lower().endswith(('avi', 'rmvb', 'mp4', 'mkv', *video)):
            return True
        return False

    def file_filter(self, name, pattern=None, **kwargs):
        """文件过滤器，正则匹配, 默认处理字母开头，后面带数字的文件
        pattern: 正则表达式
        name: 必须是文件的名字，不包含路径
        """
        if pattern is None:
            return True
        if not self.is_video_type(name):
            return False
        if re.search(pattern, name):
            return True
        return False

    @classmethod
    def name_matcher_search(cls, str_, pattern: dict, save_None=False, **kwargs):
        """返回匹配到字符的第1个"""
        target_dict = {}
        for key, values in pattern.items():
            result = re.findall(values, str_, **kwargs)
            if result:
                target_dict.setdefault(key, result[0])
            elif save_None:
                target_dict.setdefault(key, None)
            else:
                cls.MatcherError(f'表达式:[{values}]匹配结果为None')
        else:
            return target_dict

    class MatcherError(Exception):
        pass
